为了加快页面渲染,我们常使用先缓存后网络(
StaleWhileRevalidate)策略将本地缓存作为响应快速返回给用户,同时从网络中获取最新资源并更新缓存,最后通知页面进行更新。由于 Service Worker 与页面运行在不同的线程环境中,故需要一种机制来保证缓存更新后页面能够及时得到通知。在 Workbox 中,我们可以使用workbox-broadcast-cache-update模块来实现这一需求,接下来就让我们一起来探究该模块的使用
# 基本使用
workbox.routing.registerRoute(
'/articles',
new workbox.strategies.StaleWhileRevalidate({
plugins: [
new workbox.broadcastUpdate.Plugin({
channelName: 'workbox',
deferNoticationTimeout: 1000,
headersToCheck: ['Content-Length', 'ETag', 'Last-Modified']
})
]
})
);
上例中,当请求
/articles的缓存更新后,只要新响应头信息中Content-Length、ETag或Last-Modified的值有任何一个与旧响应头信息中相关属性的值不一致,便会向频道 workbox 广播缓存更新消息。其中workbox.broadcastUpdate.Plugin构造函数的参数为含有以下属性的对象:
channelName:频道名称(默认值为workbox)。headersToCheck:出于效率的考量,Workbox 通过比对前后两个响应的头信息来判断响应是否更新,我们可通过该属性来设置需比对的头信息(默认值为content-length、etag和last-modified)。deferNoticationTimeout:当请求为导航请求,且相关缓存有所更新时,Workbox 会延迟广播直到页面准备妥当(页面可通过调用navigator.serviceWorker.controller.postMessage发送{type: 'WINDOW_READY', meta: 'workbox-window'}消息来告知Workbox),同时也为了避免无限制地等待,我们可通过该属性以要求 Workbox 在等待指定时间后,无论是否收到页面通知,都将立即广播更新消息(默认值为1000,单位为毫秒)
由于
workbox.broadcastUpdate.Plugin内部使用了workbox.broadcastUpdate.BroadcastCacheUpdate来处理缓存更新广播,因此在自定义的请求策略中,可直接使用它来处理缓存更新广播,比如:
const broadcastUpdate = new workbox.broadcastUpdate.BroadcastCacheUpdate({
channelName: 'workbox',
deferNoticationTimeout: 1000,
headersToCheck: ['Content-Length', 'ETag', 'Last-Modified']
});
const cacheName = 'cacheName';
const url = 'http:/127.0.0.1:8080/articles';
const cache = await caches.open(cacheName);
const oldResponse = await cache.match(url);
const newResponse = await fetch(url);
broadcastUpdate.notifyIfUpdated({
oldResponse,
newResponse,
url,
cacheName
});
上例中,我们通过调用
workbox.broadcastUpdate.BroadcastCacheUpdate实例的notifyIfUpdated方法,以便在缓存更新后广播相关信息。该方法的参数为含有以下属性的对象:
oldResponse:已经缓存的请求响应。newResponse:新的将要被缓存的请求响应。url:请求的 URL(字符串,非 URL 类型)。cacheName:缓存名称。event:触发请求的 FetchEvent 对象,该属性为可选属性。
上文对 Service Worker 中的处理进行了介绍,此处需要牢记:无论是使用
workbox.broadcastUpdate.Plugin还是workbox.broadcastUpdate.BroadcastCacheUpdate,由于它们都是根据响应头的差异来判断缓存是否需要更新,因此如果我们将它们作用在不透明响应上,更新广播将永远不会触发。
完成了 Service Worker 中的设置,接下来就需要在页面中监听此消息,相关代码如下:
if ('BroadcastChannel' in window) {
const workboxChannel = new BroadcastChannel('workbox');
workboxChannel.addEventListener('message', event => {
console.log('Receive message from ServiceWorker:', event.data);
});
} else {
navigator.serviceWorker.addEventListener('message', event => {
console.log('Receive message from ServiceWorker:', event.data);
});
}
上例中,如果浏览器支持 BroadcastChannel API,则监听 BroadcastChannel 的 message 事件,否则监听
navigator.serviceWorker的 message 事件,回调参数 event.data 为含有以下属性的对象:
type:消息类型,值为常量CACHE_UPDATED。meta:元属性,值为常量workbox-broadcast-cache-update。payload:缓存相关信息,值为含有以下属性的对象:cacheName:缓存名称。updatedUrl:已更新的缓存地址(字符串,非 URL 类型)
# 消息总线
上文提到了
BroadcastChannel,通过它我们可以实现Service Worker与页面的相互通信,当然也可使用postMessage或MessageChannel来实现此功能。究竟这三种技术有何区别?它们各自的适用场景又该如何?本节将为一一为大家进行介绍。
# postMessage
通过 postMessage 可实现不同窗口(比如:iframe、WebWorker 或 ServiceWorker)间的相互通信。同时,由于该方法允许来自不同源的脚本进行有效且安全地通信,它也常作为跨域通信的有效解决方案。此方法的使用如下:
otherWindow.postMessage(message, targetOrigin, transferList);
otherWindow:其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open所返回的窗口对象、已命名或数值索引的window.frames、Worker或ServiceWorker实例。message:需要发送给otherWindow的数据,其值为可被结构化克隆算法序列化的所有类型。targetOrigin:通过该参数可控制消息能够发送给哪些窗口;只有目标窗口的协议、Host 地址及端口号与该参数的值完全相同,此窗口才能接收信息;当值为 * 时,则表明任何窗口都可以接收信息。transferList:可选参数,Transferable对象数组,这些对象的所有权将转移给消息的接收方,并且在所有权转移之后,消息的发送方将不能再操作该对象,否则将抛出异常。
于 Worker 或 ServiceWorker 与注册它的页面遵循同源策略,因此它们的实例方法 postMessage 的参数与上述有所差异,依次为 message 和 transferList,类型则与上述相关参数相同。
目标窗口可通过监听 message 事件来接收消息:
addEventListener('message', event => {
// doSomething...
});
回调参数 event 为 MessageEvent 对象,主要包含以下属性:
data:从其他窗口发送的消息对象。origin:消息发送窗口的源。source:消息发送窗口的引用。
# MessageChannel
除了 postMessage,我们也可以使用 MessageChannel 来实现不同窗口间的相互通信,它与 postMessage 的主要差异有:
MessageChannel的通信双方必须遵循同源策略,不能进行跨域通信。MessageChannel无需维护通信双方实体的引用。MessageChannel的使用如下所示:
// index.html
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = event => {
// doSomething...
};
navigator.serviceWorker.controller.postMessage(
'Message from UI thread',
[messageChannel.port2]
);
//sw.js
self.addEventListener('message', event => {
// doSomething...
event.ports[0].postMessage('Message back from Service Worker');
});
上例中,我们在页面中创建了信道实例 messageChannel,然后通过信道的两个端口 messageChannel.port1 和 messageChannel.port2 完成不同窗口的通信。端口的使用规则如下:
- 创建信道的窗口(此处为 index.html)使用端口 port1,并设置此端口的 onmessage 属性来接收另一端口发送的消息;
- 调用 postMessage(此处为
navigator.serviceWorker.controller.postMessage)方法将端口 port2 作为参数transferList的值,以消息的形式发送给另一个窗口(此处为sw.js); - 在
sw.js中监听 message 事件,并通过event.ports[0]来获得从 index.html 传递过来的信道端口,然后便可调用端口的postMessage方法来发送消息给index.html。
其中:
event参数为 MessageEvent 对象,主要属性上文已进行说明,此处不再重述。postMessage方法的参数依次为 message 和 transferList,具体类型上文已进行说明,此处不再重述。
# BroadcastChannel
虽然利用 MessageChannel,我们无需维护通信双方实体的引用便可完成双方的通信,但它依旧存在以下问题:
- 由于通信双方必须持有同一信道的不同端口,所以创建信道的一方必须通过某种方式将端口传递给另一方,这在无形之中增加了代码的复杂度。
- 由于同一通道只有两个端口,如果通信实体大于两个,那么
MessageChannel将无法处理。
我们可以使用 BroadcastChannel 来解决上述问题,比如:
//index.html
const broadcastChannel = new BroadcastChannel('workbox');
broadcastChannel.addEventListener('message', event => {
//...doSomething
});
broadcastChannel.postMessage('Message from UI thread');
//sw.js
const broadcastChannel = new BroadcastChannel('workbox');
broadcastChannel.addEventListener('message', event => {
//...doSomething
});
broadcastChannel.postMessage('Message back from Service Worker');
上例中,我们在 index.html 和 sw.js 中创建了具有相同名称 workbox 的广播信道实例 broadcastChannel,然后通过调用实例方法 postMessage 来发送消息,并监听实例的 message 事件来接收消息。只要保证信道具有相同的名称,通信的任何一方无需再向另一方传递任何信息,便能接收到发送方发送的消息。基于此,该机制常作为不同窗口通信的首选方案,但它依旧存在以下限制:
BroadcastChannel的通信双方必须遵循同源策略,不能进行跨域通信。- 不同于
MessageChannel,如果消息接收方在发送方发送消息之后才监听 message 事件,那么接收方将无法获得之前发送的消息。
其中:
event参数为 MessageEvent 对象,主要属性上文已进行说明,此处不再重述。postMessage方法的参数只有 message,具体类型上文已进行说明,此处不再重述。
# 总结
本章我们首先对
workbox-broadcast-cache-update模块进行了介绍,通过该模块,我们可以在请求缓存发生更新后,页面主线程能够及时得到通知;然后,我们对不同窗口通信的常见技术进行了介绍:
postMessage:主要用于跨域通信,但通信双方需要各自维护通信另一方实体的引用;MessageChannel:无需维护通信双方实体的引用,但不能处理跨域通信,通信双方需要各自持有信道的一个端口,也由于一个信道只有两个端口,因此无法处理两个以上通信实体的相互通信;主要用于一对一,且消息接收方在发送方发送消息之后才设置 onmessage 参数时,依旧需要接收到之前发送的消息的场景。BroadcastChannel:使用方式最为简单,且可以支持任意窗口(大于等于二)的相互通信,但不能处理跨域通信,并且如果消息接收方在发送方发送消息之后才监听 message 事件,那么接收方将无法获得之前发送的消息。
至此,我们完成了 Workbox 中缓存处理相关所有内容的学习,下一章,我们将对 Workbox 的后台同步进行介绍